【算法】 字符串相似度问题
之前有说过最长公共子序列的问题,类似的还有一个两个字符串相似度的问题。
所谓相似度就是指一个字符串要至少通过多少次变化(插入一个新字符,删除一个字符,替换一个字符)才能变成另一个字符串。
在python中,我们有Levenshtein模块可以非常快速地得到结果:Levenshtein.distance("string1","string2"),而如果想要通过算法自己解决,它和LCS问题类似,也是一个动态规划可以完成的问题。
算法描述:
有字符串a,b。两者长度len(a),len(b)分别是m,n。
取a,b的首字母进行比较,如果相同则操作数C=0,否则C=1,再各取两个字符串的第二个、第三个字符依次比较…当比较到a[i],b[i]这两个字符时,有:
1. 若两字符相同,则此时操作数应该等于a[i-1],b[i-1]两者的操作数,因为相比于这两者,现在的情况只不过是在两者的末尾又都添加了一个相同字符,对于计算操作数而言并不增加。
2. 若两字符不同,考虑此时的操作数C的来源可能性有三种:
① a[i-1],b[i]比较的操作数 C0 + 1,好比是认为a[i]和b[i]是a[i-1]和b[i]比较的基础上再为a[i-1]增加了一个字符,操作数+1
② a[i],b[i-1]比较的操作数 C1 + 1,理解类似上面的
③ a[i-1],b[i-1]比较的操作数 C2 + 1,好比认为给a[i-1]和b[i-1]同时添加一个字符(这一步不用算操作数,因为两者同时添加的)后,再进行一次字符的替换。
综合来说,字符串a1a2a3…an和b1b2b3…bn比较得到的操作数C = min(C0+1,C1+1,C2+k)其中k=0(an=bn时)或1(an!=bn时)
具体实现:
和LCS问题类似,通过构建一个二维数组来实现算法。数组c的行数和列数分别是两个字符串的长度+1,这个加上的1个字符可以看做是一个空字符加在两个字符串最前面,在构建数组的时候充当边界条件判断。(其实不要这个空字符貌似也可以,这样的话就是需要额外判断一下两首字母是否相同才能初始化整个数组,有点麻烦)比如比较"cherry"和"berry"的相似度,有这样一个数组:
空 | b | e | r | r | y | |
空 | 0 | 1 | 2 | 3 | 4 | 5 |
c | 1 | x | ||||
h | 2 | … | ||||
e | 3 | … | … | … | ||
r | 4 | … | ||||
r | 5 | … | … | |||
y | 6 | … | y |
数组中的每一个元素都是相对应的行列数i,j时cherry[:i]和berry[:j]之间比较的操作数。自然表格第一行和第一列是一个从0开始到字符串长度的数列,因为字符串和空串比较,总是要增加字符串长度个字符,所以总的来看就是这个数字。这样我们就可以初始化这个矩阵的第一行第一列,然后剩余部分交给上面的判断规则来填充,最后得到的最右下角的数据y便是我们要求的总的操作数了。
下面是python代码:
sa = "franknihao" sb = "takanashi" sa = " "+sa sb = " "+sb c = [[None for j in range(len(sb))] for i in range(len(sa))] for j in range(len(sb)): c[0][j] = j for i in range(len(sa)): c[i][0] = i def fill(sa,sb,c): for i in range(1,len(sa)): for j in range(1,len(sb)): if sa[i] == sb[j]: c[i][j] = c[i-1][j-1] else: c[i][j] = min(c[i-1][j]+1,c[i][j-1]+1,c[i-1][j-1]+1) fill(sa,sb,c) print c[len(sa)-1][len(sb)-1]